<!DOCTYPE html>
<html>

<head>
	<title>IndexedDB</title>
	<style>
		body {
			font-size: 0.8em;
			font-family: Sans-Serif;
		}

		form {
			background-color: #cccccc;
			border-radius: 0.3em;
			display: inline-block;
			margin-bottom: 0.5em;
			padding: 1em;
		}

		table {
			border-collapse: collapse;
		}

		input {
			padding: 0.3em;
			border-color: #cccccc;
			border-radius: 0.3em;
		}

		.required:after {
			content: "*";
			color: red;
		}

		.button-pane {
			margin-top: 1em;
		}

		#pub-viewer {
			float: right;
			width: 48%;
			height: 20em;
			border: solid #d092ff 0.1em;
		}

		#pub-viewer iframe {
			width: 100%;
			height: 100%;
		}

		#pub-list {
			width: 46%;
			background-color: #eeeeee;
			border-radius: 0.3em;
		}

		#pub-list li {
			padding-top: 0.5em;
			padding-bottom: 0.5em;
			padding-right: 0.5em;
		}

		#msg {
			margin-bottom: 1em;
		}

		.action-success {
			padding: 0.5em;
			color: #00d21e;
			background-color: #eeeeee;
			border-radius: 0.2em;
		}

		.action-failure {
			padding: 0.5em;
			color: #ff1408;
			background-color: #eeeeee;
			border-radius: 0.2em;
		}

		.note {
			font-size: smaller;
		}

		.destructive {
			background-color: orange;
		}

		.destructive:hover {
			background-color: #ff8000;
		}

		.destructive:active {
			background-color: red;
		}
	</style>
</head>

<body>
	<script type="text/javascript" src="https://cdn.bootcss.com/jquery/1.8.3/jquery.min.js"></script>

	<h1>IndexedDB Demo: storing blobs, e-publication example</h1>
	<div class="note">
		<p>
			Works and tested with:
		</p>
		<div id="compat">
		</div>
	</div>

	<div id="msg">
	</div>

	<form id="register-form">
		<table>
			<tbody>
				<tr>
					<td>
						<label for="pub-title" class="required">
							Title:
						</label>
					</td>
					<td>
						<input type="text" id="pub-title" name="pub-title" />
					</td>
				</tr>
				<tr>
					<td>
						<label for="pub-biblioid" class="required">
							Bibliographic ID:<br />
							<span class="note">(ISBN, ISSN, etc.)</span>
						</label>
					</td>
					<td>
						<input type="text" id="pub-biblioid" name="pub-biblioid" />
					</td>
				</tr>
				<tr>
					<td>
						<label for="pub-year">
							Year:
						</label>
					</td>
					<td>
						<input type="number" id="pub-year" name="pub-year" />
					</td>
				</tr>
			</tbody>
			<tbody>
				<tr>
					<td>
						<label for="pub-file">
							File image:
						</label>
					</td>
					<td>
						<input type="file" id="pub-file" />
					</td>
				</tr>
				<tr>
					<td>
						<label for="pub-file-url">
							Online-file image URL:<br />
							<span class="note">(same origin URL)</span>
						</label>
					</td>
					<td>
						<input type="text" id="pub-file-url" name="pub-file-url" />
					</td>
				</tr>
			</tbody>
		</table>

		<div class="button-pane">
			<input type="button" id="add-button" value="Add Publication" />
			<input type="reset" id="register-form-reset" />
		</div>
	</form>

	<form id="delete-form">
		<table>
			<tbody>
				<tr>
					<td>
						<label for="pub-biblioid-to-delete">
							Bibliographic ID:<br />
							<span class="note">(ISBN, ISSN, etc.)</span>
						</label>
					</td>
					<td>
						<input type="text" id="pub-biblioid-to-delete" name="pub-biblioid-to-delete" />
					</td>
				</tr>
				<tr>
					<td>
						<label for="key-to-delete">
							Key:<br />
							<span class="note">(for example 1, 2, 3, etc.)</span>
						</label>
					</td>
					<td>
						<input type="text" id="key-to-delete" name="key-to-delete" />
					</td>
				</tr>
			</tbody>
		</table>
		<div class="button-pane">
			<input type="button" id="delete-button" value="Delete Publication" />
			<input type="button" id="clear-store-button" value="Clear the whole store" class="destructive" />
		</div>
	</form>

	<form id="search-form">
		<div class="button-pane">
			<input type="button" id="search-list-button" value="List database content" />
		</div>
	</form>

	<div>
		<div id="pub-msg">
		</div>
		<div id="pub-viewer">
		</div>
		<ul id="pub-list">
		</ul>
	</div>
	<script>
		(function () {
			var COMPAT_ENVS = [
				['Firefox', ">= 16.0"],
				['Google Chrome',
					">= 24.0 (you may need to get Google Chrome Canary), NO Blob storage support"
				]
			];
			var compat = $('#compat');
			compat.empty();
			compat.append('<ul id="compat-list"></ul>');
			COMPAT_ENVS.forEach(function (val, idx, array) {
				$('#compat-list').append('<li>' + val[0] + ': ' + val[1] + '</li>');
			});

			const DB_NAME = 'mdn-demo-indexeddb-epublications';
			const DB_VERSION = 1; // Use a long long for this value (don't use a float)
			const DB_STORE_NAME = 'publications';

			var db;

			// Used to keep track of which view is displayed to avoid uselessly reloading it
			var current_view_pub_key;

			function openDb() {
				console.log("openDb ...");
				var req = indexedDB.open(DB_NAME, DB_VERSION);
				req.onsuccess = function (evt) {
					// Better use "this" than "req" to get the result to avoid problems with
					// garbage collection.
					// db = req.result;
					db = this.result;
					console.log("openDb DONE");
				};
				req.onerror = function (evt) {
					console.error("openDb:", evt.target.errorCode);
				};

				req.onupgradeneeded = function (evt) {
					console.log("openDb.onupgradeneeded");
					var store = evt.currentTarget.result.createObjectStore(
						DB_STORE_NAME, {
							keyPath: 'id',
							autoIncrement: true
						});

					store.createIndex('biblioid', 'biblioid', {
						unique: true
					});
					store.createIndex('title', 'title', {
						unique: false
					});
					store.createIndex('year', 'year', {
						unique: false
					});
				};
			}

			/**
			 * @param {string} store_name
			 * @param {string} mode either "readonly" or "readwrite"
			 */
			function getObjectStore(store_name, mode) {
				var tx = db.transaction(store_name, mode);
				return tx.objectStore(store_name);
			}

			function clearObjectStore(store_name) {
				var store = getObjectStore(DB_STORE_NAME, 'readwrite');
				var req = store.clear();
				req.onsuccess = function (evt) {
					displayActionSuccess("Store cleared");
					displayPubList(store);
				};
				req.onerror = function (evt) {
					console.error("clearObjectStore:", evt.target.errorCode);
					displayActionFailure(this.error);
				};
			}

			function getBlob(key, store, success_callback) {
				var req = store.get(key);
				req.onsuccess = function (evt) {
					var value = evt.target.result;
					if (value)
						success_callback(value.blob);
				};
			}

			/**
			 * @param {IDBObjectStore=} store
			 */
			function displayPubList(store) {
				console.log("displayPubList");

				if (typeof store == 'undefined')
					store = getObjectStore(DB_STORE_NAME, 'readonly');

				var pub_msg = $('#pub-msg');
				pub_msg.empty();
				var pub_list = $('#pub-list');
				pub_list.empty();
				// Resetting the iframe so that it doesn't display previous content
				newViewerFrame();

				var req;
				req = store.count();
				// Requests are executed in the order in which they were made against the
				// transaction, and their results are returned in the same order.
				// Thus the count text below will be displayed before the actual pub list
				// (not that it is algorithmically important in this case).
				req.onsuccess = function (evt) {
					pub_msg.append('<p>There are <strong>' + evt.target.result +
						'</strong> record(s) in the object store.</p>');
				};
				req.onerror = function (evt) {
					console.error("add error", this.error);
					displayActionFailure(this.error);
				};

				var i = 0;
				req = store.openCursor();
				req.onsuccess = function (evt) {
					var cursor = evt.target.result;

					// If the cursor is pointing at something, ask for the data
					if (cursor) {
						console.log("displayPubList cursor:", cursor);
						req = store.get(cursor.key);
						req.onsuccess = function (evt) {
							var value = evt.target.result;
							var list_item = $('<li>' +
								'[' + cursor.key + '] ' +
								'(biblioid: ' + value.biblioid + ') ' +
								value.title +
								'</li>');
							if (value.year != null)
								list_item.append(' - ' + value.year);

							if (value.hasOwnProperty('blob') &&
								typeof value.blob != 'undefined') {
								var link = $('<a href="' + cursor.key + '">File</a>');
								link.on('click', function () {
									return false;
								});
								link.on('mouseenter', function (evt) {
									setInViewer(evt.target.getAttribute('href'));
								});
								list_item.append(' / ');
								list_item.append(link);
							} else {
								list_item.append(" / No attached file");
							}
							pub_list.append(list_item);
						};

						// Move on to the next object in store
						cursor.continue();

						// This counter serves only to create distinct ids
						i++;
					} else {
						console.log("No more entries");
					}
				};
			}

			function newViewerFrame() {
				var viewer = $('#pub-viewer');
				viewer.empty();
				var iframe = $('<iframe />');
				viewer.append(iframe);
				return iframe;
			}

			function setInViewer(key) {
				console.log("setInViewer:", arguments);
				key = Number(key);
				if (key == current_view_pub_key)
					return;

				current_view_pub_key = key;

				var store = getObjectStore(DB_STORE_NAME, 'readonly');
				getBlob(key, store, function (blob) {
					console.log("setInViewer blob:", blob);
					var iframe = newViewerFrame();

					// It is not possible to set a direct link to the
					// blob to provide a mean to directly download it.
					if (blob.type == 'text/html') {
						var reader = new FileReader();
						reader.onload = (function (evt) {
							var html = evt.target.result;
							iframe.load(function () {
								$(this).contents().find('html').html(html);
							});
						});
						reader.readAsText(blob);
					} else if (blob.type.indexOf('image/') == 0) {
						iframe.load(function () {
							var img_id = 'image-' + key;
							var img = $('<img id="' + img_id + '"/>');
							$(this).contents().find('body').html(img);
							var obj_url = window.URL.createObjectURL(blob);
							$(this).contents().find('#' + img_id).attr('src', obj_url);
							window.URL.revokeObjectURL(obj_url);
						});
					} else if (blob.type == 'application/pdf') {
						$('*').css('cursor', 'wait');
						var obj_url = window.URL.createObjectURL(blob);
						iframe.load(function () {
							$('*').css('cursor', 'auto');
						});
						iframe.attr('src', obj_url);
						window.URL.revokeObjectURL(obj_url);
					} else {
						iframe.load(function () {
							$(this).contents().find('body').html("No view available");
						});
					}

				});
			}

			/**
			 * @param {string} biblioid
			 * @param {string} title
			 * @param {number} year
			 * @param {string} url the URL of the image to download and store in the local
			 *   IndexedDB database. The resource behind this URL is subjected to the
			 *   "Same origin policy", thus for this method to work, the URL must come from
			 *   the same origin as the web site/app this code is deployed on.
			 */
			function addPublicationFromUrl(biblioid, title, year, url) {
				console.log("addPublicationFromUrl:", arguments);

				var xhr = new XMLHttpRequest();
				xhr.open('GET', url, true);
				// Setting the wanted responseType to "blob"
				// http://www.w3.org/TR/XMLHttpRequest2/#the-response-attribute
				xhr.responseType = 'blob';
				xhr.onload = function (evt) {
					if (xhr.status == 200) {
						console.log("Blob retrieved");
						var blob = xhr.response;
						console.log("Blob:", blob);
						addPublication(biblioid, title, year, blob);
					} else {
						console.error("addPublicationFromUrl error:",
							xhr.responseText, xhr.status);
					}
				};
				xhr.send();

				// We can't use jQuery here because as of jQuery 1.8.3 the new "blob"
				// responseType is not handled.
				// http://bugs.jquery.com/ticket/11461
				// http://bugs.jquery.com/ticket/7248
				// $.ajax({
				//   url: url,
				//   type: 'GET',
				//   xhrFields: { responseType: 'blob' },
				//   success: function(data, textStatus, jqXHR) {
				//     console.log("Blob retrieved");
				//     console.log("Blob:", data);
				//     // addPublication(biblioid, title, year, data);
				//   },
				//   error: function(jqXHR, textStatus, errorThrown) {
				//     console.error(errorThrown);
				//     displayActionFailure("Error during blob retrieval");
				//   }
				// });
			}

			/**
			 * @param {string} biblioid
			 * @param {string} title
			 * @param {number} year
			 * @param {Blob=} blob
			 */
			function addPublication(biblioid, title, year, blob) {
				console.log("addPublication arguments:", arguments);
				var obj = {
					biblioid: biblioid,
					title: title,
					year: year
				};
				if (typeof blob != 'undefined')
					obj.blob = blob;

				var store = getObjectStore(DB_STORE_NAME, 'readwrite');
				var req;
				try {
					req = store.add(obj);
				} catch (e) {
					if (e.name == 'DataCloneError')
						displayActionFailure("This engine doesn't know how to clone a Blob, " +
							"use Firefox");
					throw e;
				}
				req.onsuccess = function (evt) {
					console.log("Insertion in DB successful");
					displayActionSuccess();
					displayPubList(store);
				};
				req.onerror = function () {
					console.error("addPublication error", this.error);
					displayActionFailure(this.error);
				};
			}

			/**
			 * @param {string} biblioid
			 */
			function deletePublicationFromBib(biblioid) {
				console.log("deletePublication:", arguments);
				var store = getObjectStore(DB_STORE_NAME, 'readwrite');
				var req = store.index('biblioid');
				req.get(biblioid).onsuccess = function (evt) {
					if (typeof evt.target.result == 'undefined') {
						displayActionFailure("No matching record found");
						return;
					}
					deletePublication(evt.target.result.id, store);
				};
				req.onerror = function (evt) {
					console.error("deletePublicationFromBib:", evt.target.errorCode);
				};
			}

			/**
			 * @param {number} key
			 * @param {IDBObjectStore=} store
			 */
			function deletePublication(key, store) {
				console.log("deletePublication:", arguments);

				if (typeof store == 'undefined')
					store = getObjectStore(DB_STORE_NAME, 'readwrite');

				// As per spec http://www.w3.org/TR/IndexedDB/#object-store-deletion-operation
				// the result of the Object Store Deletion Operation algorithm is
				// undefined, so it's not possible to know if some records were actually
				// deleted by looking at the request result.
				var req = store.get(key);
				req.onsuccess = function (evt) {
					var record = evt.target.result;
					console.log("record:", record);
					if (typeof record == 'undefined') {
						displayActionFailure("No matching record found");
						return;
					}
					// Warning: The exact same key used for creation needs to be passed for
					// the deletion. If the key was a Number for creation, then it needs to
					// be a Number for deletion.
					req = store.delete(key);
					req.onsuccess = function (evt) {
						console.log("evt:", evt);
						console.log("evt.target:", evt.target);
						console.log("evt.target.result:", evt.target.result);
						console.log("delete successful");
						displayActionSuccess("Deletion successful");
						displayPubList(store);
					};
					req.onerror = function (evt) {
						console.error("deletePublication:", evt.target.errorCode);
					};
				};
				req.onerror = function (evt) {
					console.error("deletePublication:", evt.target.errorCode);
				};
			}

			function displayActionSuccess(msg) {
				msg = typeof msg != 'undefined' ? "Success: " + msg : "Success";
				$('#msg').html('<span class="action-success">' + msg + '</span>');
			}

			function displayActionFailure(msg) {
				msg = typeof msg != 'undefined' ? "Failure: " + msg : "Failure";
				$('#msg').html('<span class="action-failure">' + msg + '</span>');
			}

			function resetActionStatus() {
				console.log("resetActionStatus ...");
				$('#msg').empty();
				console.log("resetActionStatus DONE");
			}

			function addEventListeners() {
				console.log("addEventListeners");

				$('#register-form-reset').click(function (evt) {
					resetActionStatus();
				});

				$('#add-button').click(function (evt) {
					console.log("add ...");
					var title = $('#pub-title').val();
					var biblioid = $('#pub-biblioid').val();
					if (!title || !biblioid) {
						displayActionFailure("Required field(s) missing");
						return;
					}
					var year = $('#pub-year').val();
					if (year != '') {
						// Better use Number.isInteger if the engine has EcmaScript 6
						if (isNaN(year)) {
							displayActionFailure("Invalid year");
							return;
						}
						year = Number(year);
					} else {
						year = null;
					}

					var file_input = $('#pub-file');
					var selected_file = file_input.get(0).files[0];
					console.log("selected_file:", selected_file);
					// Keeping a reference on how to reset the file input in the UI once we
					// have its value, but instead of doing that we rather use a "reset" type
					// input in the HTML form.
					//file_input.val(null);
					var file_url = $('#pub-file-url').val();
					if (selected_file) {
						addPublication(biblioid, title, year, selected_file);
					} else if (file_url) {
						addPublicationFromUrl(biblioid, title, year, file_url);
					} else {
						addPublication(biblioid, title, year);
					}

				});

				$('#delete-button').click(function (evt) {
					console.log("delete ...");
					var biblioid = $('#pub-biblioid-to-delete').val();
					var key = $('#key-to-delete').val();

					if (biblioid != '') {
						deletePublicationFromBib(biblioid);
					} else if (key != '') {
						// Better use Number.isInteger if the engine has EcmaScript 6
						if (key == '' || isNaN(key)) {
							displayActionFailure("Invalid key");
							return;
						}
						key = Number(key);
						deletePublication(key);
					}
				});

				$('#clear-store-button').click(function (evt) {
					clearObjectStore();
				});

				var search_button = $('#search-list-button');
				search_button.click(function (evt) {
					displayPubList();
				});

			}

			openDb();
			addEventListeners();

		})(); // Immediately-Invoked Function Expression (IIFE)
	</script>
</body>

</html>